iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
自我挑戰組

從零開始的AI學習之路:非本科轉職的30天挑戰記系列 第 17

D17 | 我的Side Project每拍呷--研究Azure服務篇

  • 分享至 

  • xImage
  •  

今日進度3/14:
設定Azure AI Services(或 Azure Cognitive Services for Vision) 的OCR服務。
撰寫一個簡單的Python程式碼,使用一個營養標示圖片進行OCR服務呼叫,並印出返回的JSON結構,確認服務能正常運作。


一開始先在Vision Studio試用OCR服務,但不管是用筆電前鏡頭上傳或是直接上傳電腦裡的圖片,都會有辨識不出來或是數據錯誤的問題,或是繁中辨識錯誤(我的蜜棗乾怎麼變成蛋蛋乾😫)

https://ithelp.ithome.com.tw/upload/images/20250822/20177974AiAqLgRQBT.png

看了JSON檔感覺很混亂,因為我沒用過OCR服務,我把一樣的照片丟去ChatGPT卻能解析正確,所以我問它這是正常的嗎?以下是它的回覆:

我這邊跟Azure OCR那種「電腦視覺」完全不一樣。
🔹我用到的技術其實是:圖像理解(Image Understanding):我能看懂圖片裡的文字和排版,像人類一樣「讀」出來,而不是逐像素辨識。
自然語言處理(NLP):我把圖裡的表格資訊轉換成結構化數據(例如「每份 218 大卡」),然後再進行計算。
邏輯運算:當你問「6份是多少」時,我就用簡單的乘法幫你算出答案。
換句話說,我沒有跑OCR,而是直接「看懂」圖片中的營養標示內容。這就是為什麼我不會遇到Azure OCR那種「數字亂跳、空值」的情況。
👉 所以可以理解成:
Azure OCR → 先看圖 → 轉成文字 → 你自己再整理數據。
我這邊 → 直接理解圖 → 自動幫你抓重點並算數字。

然後我請它推薦比OCR更適合我的專案的服務,它推薦了Azure Document Intelligence(前身叫 Form Recognizer),這是專門處理文件的服務,特別適合表格、發票、報表之類結構化文檔。它的優勢在於能識別文字之外,還能提取「欄位名稱 + 對應值」的 key-value 結構,非常方便輸出 JSON 格式,用於自動化流程。

於是我就改用Document Intelligence,這樣也能避免萬一用戶拍照模糊導致OCR辨識錯誤的問題。

然後我發現Gemini的Gem原來已經免費開放使用了,立刻開一個Gem來專門處理這個專案!
目前為止跟Gemini的協作都還算滿意,以下的流程都是在Gem裡面討論出來的:

👽=我
🫡=Gemini

👽我決定要改用Document Intelligence文件智慧服務,而非OCR!
🫡這個改動方向非常正確!既然舊的OCR辨識效果不佳,果斷換成更強大的Azure AI文件智慧服務是提升準確度的關鍵一步。它不只是單純的文字辨識,更擅長處理結構化的文件,這對我們擷取營養標示的表格數據來說是個大優勢。
舊的OCR服務可能更偏向於通用的文字辨識,對於不規則的字體、複雜的背景或表格格式表現不佳。但文件智慧服務專為處理文件而設計,它能:

  • 精確辨識表格與欄位: 營養標示的格式通常是表格,文件智慧服務能自動偵測表格結構,並辨識出「每份」、「每100克」、「熱量」、「蛋白質」等欄位,並將其與對應的數值配對。
  • 提取關鍵-數值配對:不僅是表格,即使是非表格的文字,它也能將「鈉含量:xxx 毫克」這類資訊準確地配對擷取。
  • 優化後的讀取模型:它的「讀取」模型針對文件處理進行了優化,能更精準地辨識印刷體、手寫字,以及高密度的文字。這對那些字體小、排版密集的營養標示特別有用。

👽接下來的開發計畫(第3天)
🫡好的,既然決定轉換技術,我們的開發計畫也需要調整。接下來的重點將是專注於整合文件智慧服務,並設計一個更穩固的數據處理流程。

  1. 調整數據擷取流程
  • 使用文件智慧服務API: 我們會用Python呼叫Azure AI文件智慧服務的Layout API(版面配置)或Read API (讀取),將用戶上傳的營養標示圖片傳送過去。
  • 處理回傳的JSON數據:文件智慧服務會回傳一個包含文字、表格、欄位位置等資訊的JSON檔案。我們需要編寫程式碼,從這個結構化數據中精準地提取出我們需要的營養素(例如:熱量、蛋白質、脂肪、碳水化合物、鈉、糖)及其對應的數值。
  1. 建立資料正規化與驗證機制
  • 處理單位差異:營養標示上的單位可能多樣化,例如:「g」、「mg」、「每份」、「每100公克」。我們的程式必須能聰明地辨識這些單位,並將所有數據正規化為統一的標準(例如:每份的克數或毫克數),才能進行後續的計算。
  • 處理數據缺失:如果圖片模糊導致特定營養素無法辨識,我們需要設計一個錯誤處理機制,提示用戶重新拍攝或手動輸入。

👽那要去哪裡可以試用這個服務?
🫡Azure AI 文件智慧服務 (Document Intelligence) 有它專屬的視覺化工具,叫做 Document Intelligence Studio。它不像 Vision Studio 那樣是通用型的,而是專門為了處理文件、表格、表單等結構化資料而設計的。
在網站裡,你會看到幾個不同的模型可以試用,我們需要的是 「Read」 或 「Layout」 這兩個模型。
Read (閱讀):專門用來擷取高品質的列印和手寫文字。
Layout (版面配置):這個更強大,它不僅能擷取文字,還能同時辨識表格、標籤、選取標記等結構化資訊。
將營養標示的圖片上傳到「Layout」模型,你就能看到它回傳的結構化 JSON 數據,裡面會清楚標示出每個表格、行、列的資訊,這會比通用型 OCR 的純文字辨識結果更有用。
👽太好了!那我要用Layout功能,感覺超級聰明!


決定使用Latout後,就開啟VS Code來測試囉!

步驟:

1. 終端機安裝 pip install azure-ai-documentintelligence
2. 初始化客戶端
3. 讀取圖片
4. 呼叫分析功能
5. 處理結果:API會回傳一個操作物件。需要等待分析完成,然後從結果中提取出我們需要的表格 (tables) 和內容 (content)。
6. 解析數據:從回傳的JSON結構化數據中,找到表格裡的「蛋白質」、「碳水化合物」等欄位,並取出它們對應的數值。

*但因為串接LINE BOT不是今天的進度,所以我先把程式碼設計成用tkinter可以呈現的(直接開啟視窗讓我上傳檔案)

最後再加入一個def函式extract_nutrition_data,以利從tables中提取這些數據:

def extract_nutrition_data(tables):
    if not tables:
        return None

    # 假設營養標示總是在第一個表格
    nutrition_table = tables[0]
    
    data = {}
    
    # 建立一個從營養素名稱到其在表格中行索引的對應表
    nutrient_row_mapping = {
        '熱量': 4,
        '蛋白質': 5,
        '脂肪': 6,
        '碳水化合物': 9,
        '糖': 10,
        '鈉': 11
    }

    # 遍歷表格中的所有單元格
    for cell in nutrition_table.cells:
        # 尋找關鍵營養素的名稱
        if cell.row_index in nutrient_row_mapping.values() and cell.column_index == 0:
            nutrient_name = cell.content.replace(' ', '')
            
            # 找到對應行的「每份」和「每100公克」數值
            for other_cell in nutrition_table.cells:
                if other_cell.row_index == cell.row_index and other_cell.column_index == 1:
                    data[f'{nutrient_name}_per_serving'] = other_cell.content
                elif other_cell.row_index == cell.row_index and other_cell.column_index == 2:
                    data[f'{nutrient_name}_per_100g'] = other_cell.content
    
    # 這裡可以加上數據清洗的邏輯,將字串轉成數字
    cleaned_data = {}
    for key, value in data.items():
        # 移除單位,例如 '大卡', '公克', '毫克'
        numeric_value = value.replace(' 大卡', '').replace(' 公克', '').replace(' 毫克', '').replace(' ', '')
        try:
            cleaned_data[key] = float(numeric_value)
        except ValueError:
            cleaned_data[key] = numeric_value # 如果無法轉換,保留原始字串
            
    return cleaned_data

# 範例使用
# from the OCR result, get tables object
# extracted_data = extract_nutrition_data(result.tables)
# print(extracted_data)

終於抓到營養數據了✨

{
    "熱量_per_serving": 180.0,
    "熱量_per_100g": 564.0,
    "蛋白質_per_serving": 2.3,
    "蛋白質_per_100g": 7.2,
    "脂肪_per_serving": 11.2,
    "脂肪_per_100g": 35.0,
    "碳水化合物_per_serving": 17.6,
    "碳水化合物_per_100g": 55.0,
    "糖_per_serving": 1.2,
    "糖_per_100g": 3.6,
    "鈉_per_serving": 187.0,
    "鈉_per_100g": 585.0
}

資訊很正確!試了用筆電前鏡頭也可以。請讓我讚嘆它的聰明/images/emoticon/emoticon07.gif

以上再融合昨天寫好的GRADIO版本程式碼,讓用戶輸入身高、體重、性別、吃了一包/半包/四分之一包:

https://ithelp.ithome.com.tw/upload/images/20250822/20177974MyhW3TwtuV.png
https://ithelp.ithome.com.tw/upload/images/20250822/20177974rhvbse8yQz.png
https://ithelp.ithome.com.tw/upload/images/20250822/20177974yEay0BmWIG.png

👽我換了一個測試資料,結果遇到了「脂肪」辨識錯誤,正確是22.4克,但顯示0克,我猜是因為脂肪的下方還分為飽和脂肪跟反式脂肪,所以辨識錯誤?
🫡你說的沒錯,要解決這個問題,我們需要讓程式碼能更智慧地處理「脂肪」這個特殊情況。最簡單也最有效的方法是:
優先尋找「脂肪」這個關鍵字:這是我們的首選,如果找到了,就提取它的數值。
如果沒有找到「脂肪」:那麼退而求其次,將「飽和脂肪」和「反式脂肪」的數值相加,作為「總脂肪」。

# --- 針對脂肪的特殊處理 ---
    total_fat_per_serving = cleaned_data.get('脂肪_per_serving_str', 0)
    
    if total_fat_per_serving == 0:
        # 如果總脂肪為0,嘗試將飽和脂肪和反式脂肪相加
        saturated_fat = cleaned_data.get('飽和脂肪_per_serving_str', 0)
        trans_fat = cleaned_data.get('反式脂肪_per_serving_str', 0)
        total_fat_per_serving = saturated_fat + trans_fat
        
    cleaned_data['脂肪_per_serving_str'] = total_fat_per_serving

    return cleaned_data, None

https://ithelp.ithome.com.tw/upload/images/20250822/201779744EZndTLzJG.png

這一版終於成功了!今天也修正了很多東西,超有趣的🤣

明日進度:

由於明天原訂進度於今天提前做完,故進度改為:
在LINE Developers後台設定BOT的基本資訊、撰寫基於 Python Flask 或 FastAPI 的簡單後端程式,使其能接收 LINE 傳來的文字訊息,並回覆「哈囉!」等基本文字。

加油!!


上一篇
D16 | 我的Side Project每拍呷--視覺化資訊篇
下一篇
D18 | 我的Side Project 每拍呷--LINE BOT篇
系列文
從零開始的AI學習之路:非本科轉職的30天挑戰記30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言